package cc.android.testapp.xp.hooker;

import android.app.Application;
import android.util.Log;

import org.json.JSONObject;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import cc.android.testapp.cfg.CFG;
import cc.android.testapp.cfg.Config;
import cc.android.testapp.cfg.ConfigHelper;
import cc.android.testapp.util.CLog;
import cc.android.testapp.xp.HookBase;
import cc.android.testapp.xp.HookModule;
import cc.commons.util.reflect.MethodUtil;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

@HookModule(name = "日志重定向")
public class LogRedirect extends HookBase implements ConfigHelper.IConfig {

    /**
     * 需要重定向的日志等级
     */
    private static final List<String> mLogLevel = Arrays.asList("i", "d", "v", "w", "e");
    /**
     * 重定向后的日志Tag前缀
     */
    private static final String TAGD = "TestApk";

    /**
     * 是否重定向日志
     */
    @CFG(key = Config.KEY_REDIRECT_LOG)
    public static boolean Enable = false;
    /**
     * 重定向日志的钩子
     */
    private final static ArrayList<Unhook> mLogHook = new ArrayList<>();
    /**
     * 用于防止重复输出日志的标志位
     */
    private static boolean mRein = false;

    @Override
    protected void hookToApp(XC_LoadPackage.LoadPackageParam pParam, Application pApp) throws Throwable {
    }

    @Override
    public void onConfigChange(JSONObject pJson) {
        boolean tPre = Enable;
        loadAnnoConfig(pJson);

        // 由于FORCE_LOG_DIRECT初始值为false,因此不存在一开始就同为true却不重定向日志的情况
        if (Enable != tPre) {
            checkRedirectLog();
        }
    }

    @Override
    public String getCfgNode() {
        return "";
    }

    /**
     * 用于检测是否为爱加密加固,如果是,重定向日志为 DEBUG 级别
     * <p>不重定向TAG中包含posed字符的日志(ps: 由于lsposed的日志名字不是xposed且api中无法直接调用XposedBridge.TAG)<p/>
     *
     * @param pParam 准备hookapp的xposed参数
     */
    public static void checkRedirectLog() {
        if (!mLogHook.isEmpty()) {
            for (Unhook sHook : mLogHook) sHook.unhook();
            mLogHook.clear();
        }

        if (!Enable) return;
        Log.e(TAGD, "Enable log redirect");

        for (Method sM : MethodUtil.getMethod(Log.class, (m) -> Modifier.isStatic(m.getModifiers()) && mLogLevel.contains(m.getName()))) {
            Class<?>[] tP = sM.getParameterTypes();
            if (tP[1] == String.class) {
                if (tP.length == 2) {
                    mLogHook.add(LogAdapter.hook(sM, (param) -> logBridge(param, (String) param.args[1], null)));
                } else if (tP.length == 3 && tP[2] == Throwable.class) {
                    mLogHook.add(LogAdapter.hook(sM, (param) -> logBridge(param, (String) param.args[1], (Throwable) param.args[2])));
                }
            } else if (tP[1] == Throwable.class) {
                mLogHook.add(LogAdapter.hook(sM, (param) -> logBridge(param, null, (Throwable) param.args[1])));
            }
        }
    }

    private static void logBridge(MethodHookParam param, String pMsg, Throwable pExp) {
        if (mRein) return;
        mRein = true;

        try {
            boolean tToXp = false;
            String tTag = (String) param.args[0];
            if (tTag != null && tTag.toLowerCase().contains("posed")) {
                tToXp = true;
            }
            tTag = TAGD + param.method.getName() + "|" + param.args[0];

            if (pMsg != null) {
                Log.e(tTag, pMsg);
                if ((CLog.FORCE_FILE_OUTPUT || Config.enableFilelog()) && !param.args[0].toString().contains("TestApp")) {
                    //保证对所有日志包括该app通过Clog打印的日志能够全部重定向
                    //但是由于重定向后统一使用了系统Log接口输出日志,导致Clog无法处理输出到文件日志的情况
                    //因此手动强制输出到文件,又因为Clog在被重定向之前也会调用输出日志到文件
                    //所以此处需要增加判断
                    CLog.logToFile(tTag + "|" + pMsg);
                }
                if (CLog.IN_XPOSED && !tToXp) {
                    de.robv.android.xposed.XposedBridge.log("[" + tTag + "] " + pMsg);
                }
            }

            if (pExp != null) {
                String tLog = Log.getStackTraceString(pExp);
                Log.e(tTag, tLog);
                if (CLog.IN_XPOSED && !tToXp)
                    de.robv.android.xposed.XposedBridge.log("[" + tTag + "] " + tLog);
            }

            param.setResult(0);
        } finally {
            mRein = false;
        }

    }

    @Override
    public boolean isTargetApp(XC_LoadPackage.LoadPackageParam pParam) {
        return true;
    }

    private static class LogAdapter extends XC_MethodHook {
        final Consumer<MethodHookParam> mRun;

        LogAdapter(Consumer<MethodHookParam> pRun) {
            this.mRun = pRun;
        }

        @Override
        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
            this.mRun.run(param);
        }

        static Unhook hook(Method pMethod, Consumer<MethodHookParam> pRun) {
            return XposedBridge.hookMethod(pMethod, new LogAdapter(pRun));
        }
    }

    private interface Consumer<T> {
        void run(T pParam);
    }
}
